/*
 * Encryption key is in Resources/data/info.json under "key", base64 encoded, shift rotated, swapped and XOR'd with iv.
 * Encypted files have a 4 bytes header ("enc" plus a byte), which is followed by Twofish encrypted file data. The Twofish
 * CBC uses 16 byte blocks, so the file data is padded with zeros.
 */
#include "main.h"

static uint8_t haskey, pgmmv_key[16], iv[16] = { 0xA0,0x47,0xE9,0x3D,0x23,0x0A,0x4C,0x62,0xA7,0x44,0xB1,0xA4,0xEE,0x85,0x7F,0xBA };

/**
 * Decrypt the encryption key
 */
static void decrypt_key(uint8_t *key)
{
    uint32_t *words = (uint32_t*)key, t;
    int i;

    for(i = 0; i < 8; i++) {
        words[2] = ROL32(words[2], 1) & 0xffffffff;
        words[3] = ROR32(words[3] & 0xffffffff, 1);
        words[0] = ROL32(words[0], 1) & 0xffffffff;
        words[1] = ROR32(words[1] & 0xffffffff, 1);
    }
    t = words[0]; words[0] = words[2]; words[2] = t;
    t = words[1]; words[1] = words[3]; words[3] = t;
    for(i = 0; i < 16; i++) key[i] ^= iv[i];
}

/**
 * Decrypt a buffer
 */
static uint64_t decrypt_file(uint8_t *buf, uint64_t len)
{
    uint64_t i, j, h;
    uint8_t key[16], xora[16], ct[16], t;
    TwofishContext tf;

    if(!haskey || !buf || len < 20) return len;
    memcpy(key, pgmmv_key, sizeof(key));
    memcpy(xora, iv, sizeof(xora));
    h = len - buf[3] - 4;
    for(i = 0; h > 0; i++, h >>= 8) {
        t = (h ^ key[i]) & 0xff;
        key[i] = t < 1 ? 1 : t;
    }
    twofishInit(&tf, key, sizeof(key));
    for(i = 4; i < len + 20; i += 16) {
        memcpy(ct, buf + i, sizeof(ct));
        twofishDecryptBlock(&tf, ct, buf + i - 4);
        for(j = 0; j < 16; j++)
            buf[i + j - 4] ^= xora[j];
        memcpy(xora, ct, sizeof(xora));
    }
    if(memcmp(buf, "\x89PNG", 4) && memcmp(buf, "OggS", 4) && (j = (uint64_t)strlen((char*)buf)) + 20 >= len) len = j;
    else len -= 4;
    return len;
}

/* these two are used for the progress bar */
static uint64_t total, current;
/* pgmmv plugin specific variables */
static uint64_t projsize = 0;
static char *project = NULL, *textList = NULL, *textEnd = NULL;
static int tileWidth = 0, tileHeight = 0, numobj = 0, objs[4096], numtile = 0, tiles[4096], anims[4096];
typedef struct {
    int imageId, hdivCount, vdivCount, sheetId;
} resourceInfoList_t;
static resourceInfoList_t resourceInfoList[256];

/**
 * Parses text json into in-memory table
 */
static void json2mem(char *json, char *end)
{
    int id = 0, col, row;

    texts_addrow(0);

    for(; json < end; json++) {
        if(!memcmp(json, "\"id\"", 4)) json = getvalue(json + 4, &id); else
        if(id > 0 && !memcmp(json, "\"text\"", 6)) {
            if(!(row = texts_addrow(id))) return;
            for(json += 6; json < end && json[-1] != '{'; json++);
            while(json < end && *json != '}') {
                while(json < end && (*json == ' ' || *json == '\t' || *json == '\r' || *json == '\n' || *json == ',')) json++;
                if(*json == '}') break;
                if(*json != '\"') { numrow--; break; }
                if((col = texts_getcol(++json)) != -1) {
                    while(json < end && *json && json[-1] != ':') json++;
                    while(json < end && (*json == ' ' || *json == '\t' || *json == '\r' || *json == '\n' || *json == ',')) json++;
                    if(*json != '\"') { numrow--; break; }
                    texts[row].value[col] = newstr(&json);
                }
            }
            id = 0;
        }
    }
}

/**
 * Load an image asset once, decrypt if necessary
 */
static int getimg(char *path, int id)
{
    char *fn;
    uint8_t *buf = NULL;
    uint64_t len;
    int ret = -1;

    if(id < 1 || id > 999 || (ret = sprites_addsheet(id, NULL, NULL, 0)) != -1) return ret;
    fn = path + strlen(path);
    sprintf(fn, "/Resources/img/%03d.png", id);
    if((buf = getfile(path, &len))) {
        if(len > 20 && !memcmp(buf, "enc", 3)) len = decrypt_file(buf, len);
        ret = sprites_addsheet(id, fn + 11, buf, len);
        free(buf);
    }
    *fn = 0;
    return ret;
}

/**
 * Load a sound effect asset once, decrypt if necessary
 */
static int getogg(char *path, int id)
{
    char *fn;
    uint8_t *buf = NULL;
    uint64_t len;
    int ret = -1;

    if(id < 1 || id > 999 || (ret = audio_addsfx(id, NULL, NULL, 0)) != -1) return ret;
    fn = path + strlen(path);
    sprintf(fn, "/Resources/audio/se%03d.ogg", id);
    if((buf = getfile(path, &len))) {
        if(len > 20 && !memcmp(buf, "enc", 3)) len = decrypt_file(buf, len);
        /* if we can add the sound effect, decode it right away, should be short */
        if((ret = audio_addsfx(id, fn + 11, buf, len)) == -1) free(buf);
        else audio_load(ret);
    }
    *fn = 0;
    return ret;
}

/**
 * Parse the animations part of project.json
 */
static void parseanims(char *path, char *json)
{
    char *s, *frameList = NULL, name[NAMELEN];
    int numb = 0, nums = 0, blk = 0, id = 0, rid, aid = 0, sid, cnt, fps = 300, sx = 0, sy = 0, dx = 0, dy = 0, sfx = -1;
    int w, h, sw, sh;

    memset(name, 0, sizeof(name));
    for(; *json && numb >= 0 && nums >= 0; json++) {
        if(*json == '[') numb++;
        if(*json == ']') numb--;
        if(*json == '{') nums++;
        if(*json == '}') nums--;
        if(!memcmp(json, "\"resourceInfoList\"", 18)) { json += 18; blk = 1; aid = id; frameList = NULL; memset(resourceInfoList, 0, sizeof(resourceInfoList)); }
        if(!memcmp(json, "\"directionList\"", 15)) { json += 15; blk = 2; rid = 0; status(LOADING, ++current, total); }
        if(!memcmp(json, "\"id\"", 4)) { json = getvalue(json + 4, &id); }
        if(blk == 1 && id > 0 && id < 256) {
            if(!memcmp(json, "\"imageId\"", 9)) {
                json = getvalue(json + 9, &resourceInfoList[id].imageId);
                resourceInfoList[id].sheetId = getimg(path, resourceInfoList[id].imageId);
            }
            if(!memcmp(json, "\"hdivCount\"", 11))
                json = getvalue(json + 11, &resourceInfoList[id].hdivCount);
            if(!memcmp(json, "\"vdivCount\"", 11))
                json = getvalue(json + 11, &resourceInfoList[id].vdivCount);
        }
        if(blk < 2 && !memcmp(json, "\"name\"", 6)) json = getstr(json + 6, name, sizeof(name));
        if(blk == 2) {
            if(!memcmp(json, "\"resourceInfoId\"", 16)) { json = getvalue(json + 16, &rid); }
            if(!memcmp(json, "\"frameList\"", 11)) { json += 11; frameList = json; }
            if(!memcmp(json, "\"imageWidth\"", 12)) { json = getvalue(json + 12, &w); }
            if(!memcmp(json, "\"imageHeight\"", 13)) { json = getvalue(json + 13, &h);
                if(frameList && rid > 0 && rid < 256 &&
                    resourceInfoList[rid].sheetId != -1 && resourceInfoList[rid].hdivCount && resourceInfoList[rid].vdivCount) {
                    sw = sheets[resourceInfoList[rid].sheetId].w / resourceInfoList[rid].hdivCount;
                    sh = sheets[resourceInfoList[rid].sheetId].h / resourceInfoList[rid].vdivCount;
                    id = -1;
                    for(s = frameList; s < json && *s && *s != ']'; s++) {
                        if(!memcmp(s, "\"frameCount30\"", 14)) { s = getvalue(s + 14, &cnt); fps = 30; }
                        if(!memcmp(s, "\"frameCount60\"", 14)) { s = getvalue(s + 14, &cnt); fps = 60; }
                        if(!memcmp(s, "\"frameCount100\"", 15)) { s = getvalue(s + 15, &cnt); fps = 100; }
                        if(!memcmp(s, "\"frameCount120\"", 15)) { s = getvalue(s + 15, &cnt); fps = 120; }
                        if(!memcmp(s, "\"frameCount300\"", 15)) { s = getvalue(s + 15, &cnt); fps = 300; }
                        if(!memcmp(s, "\"playSeId\"", 10)) { s = getvalue(s + 10, &sid); sfx = getogg(path, sid); }
                        if(!memcmp(s, "\"offsetX\"", 9)) s = getvalue(s + 9, &dx);
                        if(!memcmp(s, "\"offsetY\"", 9)) s = getvalue(s + 9, &dy);
                        if(!memcmp(s, "\"imageTileX\"", 12)) { s = getvalue(s + 12, &sx); sx *= w; }
                        if(!memcmp(s, "\"imageTileY\"", 12)) { s = getvalue(s + 12, &sy); sy *= h; }
                        if(*s == '}') {
                            id = sprites_add(id, NULL, w, h, dx, dy, resourceInfoList[rid].sheetId, sx, sy, sw, sh, cnt * 1000 / fps, sfx);
                            sfx = -1; cnt = dx = dy = sx = sy = 0;
                            if(!sprites[id].name[0]) strncpy(sprites[id].name, name, NAMELEN - 1);
                            if(aid > 0 && aid < 4096) anims[aid] = id;
                        }
                    }
                }
                rid = aid = blk = 0; frameList = NULL; memset(name, 0, sizeof(name));
            }
        }
    }
}

/**
 * Parse the tilesets part of project.json
 */
static void parsetilesets(char *path, char *json)
{
    int numb = 0, nums = 0, i, j, id = 0;

    if(!numtile || !tileWidth || !tileHeight) return;

    if(verbose > 1) debug("tilesets (%u sets, tilewidth %u, tileheight %u)", path, numtile, tileWidth, tileHeight);
    for(i = 0; *json && numb >= 0 && nums >= 0 && i < numtile; json++) {
        if(*json == '[') numb++;
        if(*json == ']') numb--;
        if(*json == '{') nums++;
        if(*json == '}') nums--;
        if(!numb && !memcmp(json, "\"id\"", 4)) json = getvalue(json + 4, &id);
        if(id > 0 && id < 4096) {
            if(!memcmp(json, "\"imageId\"", 9)) {
                json = getvalue(json + 9, &j);
                tiles[id] = getimg(path, j);
                status(LOADING, ++current, total);
                id = 0;
            }
        }
    }
}

/**
 * Parse the objects part of project.json
 */
static void parseobjs(char *json)
{
    int numb = 0, nums = 0, i, j, id = 0;

    if(!numobj || !sprites || !numsprites) return;

    for(i = -1; *json && numb >= 0 && nums >= 0 && i < numobj; json++) {
        if(*json == '[') numb++;
        if(*json == ']') numb--;
        if(*json == '{') nums++;
        if(*json == '}') nums--;
        if(!memcmp(json, "\"id\"", 4)) json = getvalue(json + 4, &id);
        if(id > 0 && id < 4096 && !memcmp(json, "\"animationId\"", 13)) {
            json = getvalue(json + 13, &j);
            objs[id] = j > 0 && j < 4096 ? anims[j] : -1;
            status(LOADING, ++current, total);
            id = 0;
        }
    }
}

/**
 * Parse the scenes (map) part of project.json
 */
static void parsescenes(char *json)
{
    char *tile = NULL, name[NAMELEN];
    int mid = -1, i, id, numb = 0, nums = 0, blks = 0, blk = 0, tid = 0, c = 0, r = 0, x, y, w = 0, h = 0, *map = NULL;

    if(!numtile) return;

    memset(name, 0, sizeof(name));
    for(; *json && numb >= 0 && nums >= 0; json++) {
        if(*json == '[') numb++;
        if(*json == ']') numb--;
        if(*json == '{') nums++;
        if(*json == '}') nums--;
        if(!memcmp(json, "\"scenePartList\"", 15)) {
            if(tile && w > 0 && h > 0) {
                if(verbose > 1) debug("map '%s' (width %u, height %u)", name, w, h);
                mid = map_add(id, name, 0, tileWidth, tileHeight, w, h, 0);
                for(tid = -1, x = y = c = r = 0; tile < json && *tile; tile++) {
                    if(!memcmp(tile, "\"tile\"", 6)) map = map_add_under(mid); else
                    if(tile[0] == '\"' && tile[1] >= '0' && tile[1] <= '9') {
                        tile++; c = atoi(tile); while(tile < json && *tile && *tile >= '0' && *tile <= '9') tile++;
                        while(tile < json && *tile && *tile != '\"' && (*tile == ',' || *tile == ' ')) tile++;
                        r = atoi(tile);
                    } else
                    if(!memcmp(tile, "\"tilesetId\"", 11)) tile = getvalue(tile + 11, &tid); else
                    if(!memcmp(tile, "\"x\"", 3)) tile = getvalue(tile + 3, &x); else
                    if(!memcmp(tile, "\"y\"", 3)) tile = getvalue(tile + 3, &y); else
                    if(*tile == '}') {
                        /* auuuch, this hurts... let's pack these into a single integer */
                        if(tid >= 0 && tid < numtile && tiles[tid] != -1 && map)
                            map[r * w + c] = ((tiles[tid] & 4095) << 20) | ((y & 1023) << 10) | (x & 1023);
                        tid = -1; x = y = c = r = 0;
                    }
                }
            }
            tile = NULL; map = NULL; w = h = 0;
            status(LOADING, ++current, total);
            for(json += 15; *json && json[-1] != '['; json++);
            for(blks = numb++, id = x = y = 0; *json && numb > blks; json++) {
                if(*json == '[') numb++;
                if(*json == ']') numb--;
                if(!memcmp(json, "\"objectId\"", 10)) json = getvalue(json + 10, &id); else
                if(!memcmp(json, "\"x\"", 3)) { json = getvalue(json + 3, &x); } else
                if(!memcmp(json, "\"y\"", 3)) { json = getvalue(json + 3, &y);
                    if(mid != -1 && id > 0 && id < 4096 && objs[id] != -1)
                        map_sprite(mid, objs[id], x, y);
                }
            }
            mid = -1; id = 0; memset(name, 0, sizeof(name));
            blk = 0;
        } else
        if(!memcmp(json, "\"id\"", 4)) { json = getvalue(json + 4, &i); blk = 0; } else
        if(!name[0] && !memcmp(json, "\"name\"", 6)) json = getstr(json + 6, name, sizeof(name)); else
        if(!memcmp(json, "\"layer", 6) && json[6] >= '0' && json[6] <= '9') blk = 1; else
        if(blk == 1 && !memcmp(json, "\"tile\"", 6)) {
            /* count in advance how many columns and rows map has, and we'll parse it later when "scenePartList" reached */
            if(!tile) { tile = json; id = i; }
            for(json += 6; *json && json[-1] != '{'; json++);
            blk = 2; blks = nums++;
            for(; *json && nums > blks; json++) {
                if(*json == '{') nums++;
                if(*json == '}') nums--;
                if(json[0] == '\"' && json[1] >= '0' && json[1] <= '9') {
                    json++; c = atoi(json) + 1; while(*json && *json >= '0' && *json <= '9') json++;
                    while(*json && *json != '\"' && (*json == ',' || *json == ' ')) json++;
                    r = atoi(json) + 1;
                    if(w < c) w = c;
                    if(h < r) h = r;
                }
            }
        }
    }
}

/**
 * This function should return 1 if game is detected as PGMMV
 * path: full path the user provided
 * buf: if path is a directory, then NULL, otherwise the first 4096 bytes of the file
 * size: if path is a directory, then 0, otherwise total size of the file
 */
int pgmmv_detect(char *path, uint8_t *buf, uint64_t size)
{
    char tmp[PATH_MAX + FILENAME_MAX + 1];

    (void)size;
    if(!path || !*path || buf) return 0;
    strcpy(tmp, path);
    strcat(tmp, "/Resources/data/project.json");
    return fileexists(tmp);
}

/**
 * Callback function for find() in pgmmv_load()
 */
int pgmmv_loadcb(char *path)
{
    char tmp[32], *ext;
    uint8_t *buf;
    uint64_t size = 0;
    void *f = fileopen(path, &size);

    if(f) {
        /* we only read in the first 32 bytes of every file, that's enough for checking */
        fileread(f, tmp, sizeof(tmp));
        fileclose(f);
        /* get file name extension */
        ext = strrchr(path, '.');
        /* if file is of a known kind... */
        if(size > 32 && (fileknown((uint8_t*)tmp) ||
        /* ...or an encrypted non-json... */
        (ext && strcmp(ext, ".json") && !memcmp(tmp, "enc", 3))) &&
        /* ...only then load the file entirely into memory */
        (buf = getfile(path, &size))) {
            /* decrypt buffer if necessary */
            if(!memcmp(tmp, "enc", 3)) size = decrypt_file(buf, size);
            /* use the common dispatcher to detect asset type and add it to the appropriate list
             * pathlen is the length of the path the user provided, we cut that part off from asset names
             * returns 1 if buffer is still in use */
            if(!fileasset(-1, path + pathlen, buf, size)) free(buf);
        }
    }
    /* adjust and display progress bar */
    current++;
    status(LOADING, current, total);
    return 1;
}

/**
 * Load all assets from a PGMMV game
 */
void pgmmv_load(char *path, int isdir)
{
    uint8_t *buf, *key;
    uint64_t size;
    int numb = 0, blk = 0;
    char tmp[PATH_MAX + FILENAME_MAX + 1], *fn, *str;
    char *alist = NULL, *slist = NULL, *tlist = NULL, *olist = NULL;

    /* check user provided inputs */
    if(!path || !*path || !isdir) return;

    /* progress bar */
    status(LOADING, 0, 100);

    /* descending into directories will change the path string, so create a temporary copy */
    strcpy(tmp, path);
    fn = tmp + strlen(tmp);

    /* do some pgmmv specific file parsing */
    strcpy(fn, "/Resources/data/info.json");
    haskey = 0; memset(pgmmv_key, 0, sizeof(pgmmv_key));
    if(engine_keylen) {
        if(engine_keylen == sizeof(pgmmv_key)) { haskey = 1; memcpy(pgmmv_key, engine_key, sizeof(pgmmv_key)); }
        else msgbox(0, BADKEYLEN, NULL);
    }
    if((buf = getfile(tmp, &size))) {
        if(!haskey && (key = (uint8_t*)strstr((const char*)buf, "\"key\""))) {
            for(key += 6; *key && key[-1] != '\"'; key++);
            if(base64_decode((char*)key, pgmmv_key, 16) == 16) {
                decrypt_key(pgmmv_key);
                haskey = 1;
            } else
                memset(pgmmv_key, 0, sizeof(pgmmv_key));
        }
        if((key = (uint8_t*)strstr((const char*)buf, "\"title\""))) {
            getstr((char*)key + 8, engine_game, sizeof(engine_game));
        }
        free(buf);
    }
    if(haskey)
        debug("encryption key "
            "%02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
            pgmmv_key[0], pgmmv_key[1], pgmmv_key[2], pgmmv_key[3],
            pgmmv_key[4], pgmmv_key[5], pgmmv_key[6], pgmmv_key[7],
            pgmmv_key[8], pgmmv_key[9], pgmmv_key[10], pgmmv_key[11],
            pgmmv_key[12], pgmmv_key[13], pgmmv_key[14], pgmmv_key[15]);

    strcpy(fn, "/Resources");
    current = 0; total = dn(tmp);

    numobj = numtile = 0;
    memset(tiles, 0xff, sizeof(tiles)); memset(objs, 0xff, sizeof(objs)); memset(anims, 0xff, sizeof(anims));
    pathlen = (uintptr_t)fn - (uintptr_t)tmp + 10;

    /* first, we parse project.json and load all assets with the id as it appears in the json */
    strcpy(fn, "/Resources/data/project.json");
    if((project = (char*)getfile(tmp, &projsize))) {
        if(projsize > 20 && !memcmp(project, "enc", 3)) projsize = decrypt_file((uint8_t*)project, projsize);
        /* get total item counts (needed for the progress bar) */
        for(str = project; str < project + projsize && *str; str++) {
            if(*str == '[') numb++;
            if(*str == ']') numb--;
            if(!numb && *str == ',') blk = 0;
            if(!numb && !tileWidth && !memcmp(str, "\"tileWidth\"", 11)) str = getvalue(str + 11, &tileWidth);
            if(!numb && !tileHeight && !memcmp(str, "\"tileHeight\"", 12)) str = getvalue(str + 12, &tileHeight);
            if(!numb && !memcmp(str, "\"animationList\"", 15)) {
                for(alist = str + 16; *alist && alist[-1] != '['; alist++);
                blk = 1;
            }
            if(!numb && !memcmp(str, "\"sceneList\"", 11)) {
                for(slist = str + 11; *slist && slist[-1] != '['; slist++);
                blk = 2;
            }
            if(!numb && !memcmp(str, "\"tilesetList\"", 13)) {
                for(tlist = str + 13; *tlist && tlist[-1] != '['; tlist++);
                blk = 3;
            }
            if(!numb && !memcmp(str, "\"objectList\"", 12)) {
                for(olist = str + 12; *olist && olist[-1] != '['; olist++);
                blk = 4;
            }
            if(!textList && !memcmp(str, "\"textList\"", 10)) {
                for(str += 10; str < project + projsize && *str && str[-1] != '['; str++);
                textList = str;
                for(blk = 0; str < project + projsize && *str && blk >= 0; str++) {
                    if(*str == '[') blk++;
                    if(*str == ']') blk--;
                }
                textEnd = str;
                blk = 0;
            }
            if(numb && blk == 1 && !memcmp(str, "\"directionList\"", 15)) { str += 15; total++; }
            if(blk == 2 && !memcmp(str, "\"scenePartList\"", 15)) { str += 15; total++; }
            if(blk == 3 && !memcmp(str, "\"imageId\"", 9)) { str += 9; numtile++; total++; }
            if(blk == 4 && !memcmp(str, "\"animationId\"", 13)) { str += 13; numobj++; total++; }
        }
        /* do the actual parsing, these will also load assets, but with pgmmv's id */
        *fn = 0;
        if(alist) parseanims(tmp, alist);
        if(tlist) parsetilesets(tmp, tlist);
        if(olist) parseobjs(olist);
        if(slist) parsescenes(slist);
        if(textList && textEnd) json2mem(textList, textEnd);
    }

    /* iterate on remaining files and try to load assets from those. If one was already loaded by the code
     * above, then it won't be loaded twice. We don't care about pgmmv id of these */
    strcpy(fn, "/Resources");
    pathlen = strlen(tmp);
    /* recursively iterate a directory and call a callback on each files within */
    find(tmp, 1, pgmmv_loadcb);

    /* we're finished, tell what features we've detected. No need to set EF_SPRITES, EF_MAPS etc., because
     * when you successfully add an asset, those flags are set automatically */
    engine_features |= EF_DECRYPT | EF_BACKUP;
}

/**
 * Free any additional resources that pgmmv_load() allocated
 */
void pgmmv_unload(void)
{
    if(project) { free(project); project = NULL; }
    projsize = 0;
    textList = textEnd = NULL;
}

/**
 * Callback function for find() in pgmmv_decrypt()
 */
int pgmmv_decryptcb(char *path)
{
    uint8_t *buf, tmp[32];
    uint64_t size = 0;
    void *f = fileopen(path, &size);
    int len;

    current += size;
    if(f) {
        fileread(f, tmp, sizeof(tmp));
        fileclose(f);
        /* we load 32 bytes of every file for detection, and load entirely only if it's encrypted */
        if(size > 32 && !memcmp(tmp, "enc", 3)) {
            if((buf = getfile(path, &size))) {
                /* in pgmmv encrypted and decrypted files have the same name, so create a backup */
                if(engine_backup) {
                    len = strlen(path);
                    strcpy(path + len, "~");
                    debug("creating backup '%s'", path);
                    putfile(path, buf, size, NULL, 0, NULL, 0);
                    path[len] = 0;
                }
                /* if decryption successfull, write out the decrypted file */
                if((size = decrypt_file(buf, size)))
                    putfile(path, buf, size, NULL, 0, NULL, 0);
                free(buf);
            }
        }
    }
    /* display progress bar */
    status(SAVING, current, total);
    return 1;
}

/**
 * Decrypt and unpack all files in a PGMMV game
 */
void pgmmv_decrypt(char *path, int isdir)
{
    char tmp[PATH_MAX + FILENAME_MAX + 1];

    if(!path || !*path || !isdir || !haskey) return;

    /* descending into directories will change the path string, so create a temporary copy */
    strcpy(tmp, path);
    strcat(tmp, "/Resources");

    /* set up the progress bar, du() returns total disk usage in that directory */
    status(SAVING, 0, 100);
    current = 0; total = du(tmp);

    /* recursively iterate a directory and call a callback on each files within */
    find(tmp, 1, pgmmv_decryptcb);
}

/**
 * Draw map tiles to off-screen pixel buffer
 */
void pgmmv_drawmap(map_t *map)
{
    int i, l, x, y, tile, dx, dy;

    if(!map || !map->tw || !map->th) return;

    /* ordering and tile encoding is very engine specific */
    for(l = i = 0; l < map->l; l++)
        for(y = dy = 0; y < map->h; y++, dy += map->th) {
            for(x = dx = 0; x < map->w; x++, dx += map->tw) {
                tile = map->data[i++];
                map_tile(tile >> 20, (tile & 1023) * map->tw, ((tile >> 10) & 1023) * map->tw, map->tw, map->th, dx, dy);
            }
        }
    /* no need to draw the sprites, that's handled by maps.c */
}

/**
 * Save translated text
 */
void pgmmv_translate(char *path)
{
    uint64_t size;
    char tmp[PATH_MAX + FILENAME_MAX + 1], *fn, *buf, *json, *str, *src;
    int len, id, row, col;

    if(project && projsize && textList && textEnd && path) {
        strcpy(tmp, path);
        fn = tmp + strlen(tmp);
        strcpy(fn, "/Resources/data/project.json");
        /* if create backup weas requested */
        if(engine_backup && (buf = (char*)getfile(tmp, &size))) {
            strcat(fn, "~");
            putfile(tmp, (uint8_t*)buf, size, NULL, 0, NULL, 0);
            free(buf); buf = NULL;
            tmp[strlen(tmp) - 1] = 0;
        }
        /* count the required length */
        len = (uintptr_t)textEnd - (uintptr_t)textList + (16 + 10 * numcol) * numrow;
        for(row = 0; row < numrow; row++) {
            for(col = 0; col < numcol; col++)
                for(len += 3, src = texts[row].value[col]; src && *src; src++) { len++; if(*src == '\"') len++; }
        }
        if(!(buf = str = (char*)malloc(len))) {
            engine_features |= EF_NOMEM;
            debug("unable to allocate memory for json (%u bytes)", len);
            return;
        }
        /* copy patched json */
        for(json = textList; json < textEnd; json++) {
            if(!memcmp(json, "\"id\"", 4)) getvalue(json + 4, &id);
            if(id > 0 && !memcmp(json, "\"text\"", 6)) {
                for(row = 1; row < numrow && texts[row].id != id; row++);
                if(row >= numrow)
                    debug("missing translation for id %d\r\n",id);
                else {
                    for(; json < textEnd && json[-1] != '{'; json++) *str++ = *json;
                    for(; json < textEnd && *json != '}'; json++);
                    for(len = col = 0; col < numcol; col++) {
                        if(texts[row].value[col]) {
                            if(len) *str++ = ','; else len = 1;
                            str += sprintf(str, " \"");
                            memcpy(str, texts[0].value[col], 5); str += 5;
                            str += sprintf(str, "\": \"");
                            for(src = texts[row].value[col]; src && *src; src++) {
                                if(*src == '\"' || *src == '\'') *str++ = '\\';
                                *str++ = *src;
                            }
                            *str++ = '\"';
                        }
                    }
                    *str++ = ' ';
                }
                id = 0;
            }
            *str++ = *json;
        }
        *str = 0;
        /* save new json, first unmodified part of project.json, the modified part, and the last unmodified part */
        putfile(tmp,
            (uint8_t*)project, (uintptr_t)textList - (uintptr_t)project,
            (uint8_t*)buf, (uintptr_t)str - (uintptr_t)buf,
            (uint8_t*)textEnd, (uintptr_t)project + projsize - (uintptr_t)textEnd);
        free(buf);
    }
}

/* register out engine parser */
ENGINE_MODULE(pgmmv, "Pixel Game Maker MV")
